/*
	Pulsar engine. Core.
	Date & time management

	Copyright (C) 2013-2014 Dmitry Dolgov

	This program is free software; you can redistribute it and/or modify it under the terms of the
	GNU General Public License as published by the Free Software Foundation; either version 2 of
	the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
	without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
	See the GNU General Public License for more details.
*/

#include <time.h>
#include <pulsar/pulsar.h>

namespace pl
{

/// Number of days in month
static pl_uint8 _nDayMonth[12] = {31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

/// Formatting date with given format
static pl::String<16> _format(pl_uint32 value, pl_char sym, pl_uint32 symbolCount)
{
	pl::String<16> ret;

	ret = pl::int2str<16>(value, 10);

	while (sym != 0 && ret.size() < symbolCount)
	{
		ret.insert(0, &sym, 1);
	}
	return ret;
}

/// Extract next value from string (as a date)
static bool _getNextValue(pl_uint16& value, const pl_char** src, int nDigits)
{
	pl::String32 strTemp;

	value = 0;
	while (nDigits > 0)
	{
		if (!pl_isdigit(**src))
		{
			return false;
		}
		value = value*10 + (**src - PL_T('0'));
		src++;
		nDigits--;
	}
	return true;
}

} // namespace pl

/**
	Get n days in year
*/
pl_uint16 pl::DateTime::dayInYear(pl_uint16 year)
{
	return 337+dayInMonth(2, year);
}

/**
	Get n days in month
*/
pl_uint8 pl::DateTime::dayInMonth(pl_uint8 month, pl_uint16 year)
{
	if (month < 1 || month > 12)
	{
		return 0;
	}
	if (month != 2)
	{
		return _nDayMonth[month-1];
	}

	if ((year % 4) != 0)
	{
		return 28;
	}
	if ((year % 100) != 0)
	{
		return 29;
	}
	return ((year % 400) == 0) ? 29 : 28;
}

/**
	Validate date
*/
bool pl::DateTime::isValid() const
{
	if (msecond > 999)
	{
		return false;
	}
	if (second > 59)
	{
		return false;
	}
	if (minute > 59)
	{
		return false;
	}
	if (hour > 23)
	{
		return false;
	}
	if (day < 1 || day > dayInMonth(month, year))
	{
		return false;
	}
	if (month < 1 || month > 12)
	{
		return false;
	}
	return (year != 0);
}

/**
	Calculate diff between dates (in days)
*/
int pl::DateTime::diffDays(const DateTime& d1, const DateTime& d2)
{
	DateTime dFrom, dTo;
	int sign = 1;
	int delta = 0;

	// Check and swap dates
	if (d1 > d2)
	{
		dFrom = d2;
		dTo = d1;
		sign = -1;
	}
	else if (d1 < d2)
	{
		dFrom = d1;
		dTo = d2;
	}
	else
	{
		return 0;
	}

	// Scroll till Jan 1 of next year
	if (dFrom.year < dTo.year)
	{
		delta += dayInMonth(dFrom.month, dFrom.year) - (dFrom.day - 1);
		dFrom.month++;
		while (dFrom.month <= 12)
		{
			delta += dayInMonth(dFrom.month++, dFrom.year);
		}
		dFrom.year++;
		dFrom.month = 1;
		dFrom.day = 1;
	}

	// Scroll years
	while (dFrom.year < dTo.year)
	{
		delta += dayInYear(dFrom.year++);
		dFrom.month = 1;
		dFrom.day = 1;
	}

	// Scroll monthes
	while (dFrom.month < dTo.month)
	{
		delta += dayInMonth(dFrom.month++, dFrom.year);
	}

	// Add days
	delta += (dTo.day - dFrom.day);
	return sign*delta;
}

/**
	Calculate difference between dates in minutes
*/
pl_int64 pl::DateTime::diffSeconds(const DateTime& d1, const DateTime& d2)
{
	DateTime dFrom, dTo;
	int sign = 1;
	pl_int64 delta = 0;

	// Check and swap dates
	if (d1 > d2)
	{
		dFrom = d2;
		dTo = d1;
		sign = -1;
	}
	else if (d1 < d2)
	{
		dFrom = d1;
		dTo = d2;
	}
	else
	{
		return 0;
	}

	// Scroll till Jan 1 of next year
	while (dFrom.year < dTo.year)
	{
		delta += (dayInMonth(dFrom.month, dFrom.year) - (dFrom.day - 1)) * 86400;
		dFrom.month++;
		while (dFrom.month <= 12)
		{
			delta += dayInMonth(dFrom.month++, dFrom.year) * 86400;
		}
		dFrom.year++;
		dFrom.month = 1;
		dFrom.day = 1;
	}

	// Scroll years
	while (dFrom.year < dTo.year)
	{
		delta += dayInYear(dFrom.year++) * 60*60*24;
		dFrom.month = 1;
		dFrom.day = 1;
	}

	// Scroll monthes
	while (dFrom.month < dTo.month)
	{
		delta += dayInMonth(dFrom.month++, dFrom.year) * 86400;
	}

	// Add days
	delta += (dTo.day - dFrom.day) * 86400;

	// Adding hours, min and sec
	delta += (dTo.hour - dFrom.hour) * 3600;
	delta += (dTo.minute - dFrom.minute) * 60;
	delta += (dTo.second - dFrom.second);
	return sign*delta;
}

/**
	Calculate day if week
*/
int pl::DateTime::dayOfWeek() const
{
	DateTime dtStart(0, 0, 0, 0, 0, 0);

	if (year > 1997)
	{
		dtStart.year = 1997; // Wednesday
	}
	else if (year > 1986)
	{
		dtStart.year = 1986; // Wednesday
	}
	else
	{
		dtStart.year = 1930; // Wednesday
	}
	dtStart.month = 1;
	dtStart.day = 1;

	int nDays = diffDays(dtStart, *this);
	return (nDays + 2) % 7;
}

/**
	Adding delta to day
*/
const pl::DateTime pl::DateTime::operator + (const pl::DateTime& d) const
{
	int msecondTemp = 0, secondTemp = 0, minuteTemp = 0, hourTemp = 0, dayTemp = 0, monthTemp = 0, yearTemp = 0;

	msecondTemp = msecond + d.msecond;
	if (msecondTemp > 999)
	{
		secondTemp += (msecond / 1000);
		msecondTemp %= 1000;
	}
	secondTemp += (second + d.second);
	if (secondTemp > 59)
	{
		minuteTemp += secondTemp / 60;
		secondTemp %= 60;
	}
	minuteTemp += (minute + d.minute);
	if (minuteTemp > 59)
	{
		hourTemp += minuteTemp / 60;
		minuteTemp %= 60;
	}
	hourTemp += (hour + d.hour);
	if (hourTemp > 23)
	{
		dayTemp += hour % 24;
		hourTemp %= 24;
	}
	dayTemp += (day + d.day);
	monthTemp += (month + d.month);
	yearTemp += (year + d.year);
	while (dayTemp > dayInMonth((pl_uint8)monthTemp, (pl_uint16)yearTemp))
	{
		dayTemp -= dayInMonth((pl_uint8)monthTemp, (pl_uint16)yearTemp);
		monthTemp++;
		while (monthTemp > 12)
		{
			monthTemp -= 12;
			yearTemp++;
		}
	}
	while (monthTemp > 12)
	{
		monthTemp -= 12;
		yearTemp++;
	}

	DateTime res(yearTemp, monthTemp, dayTemp, hourTemp, minuteTemp, secondTemp, msecondTemp);
	PL_ASSERTW(res.isValid(), PL_T("Internal error"));
	return res;
}

const pl::DateTime pl::DateTime::operator - (const pl::DateTime& d) const
{
	int msecondTemp = 0, secondTemp = 0, minuteTemp = 0, hourTemp = 0, dayTemp = 0, monthTemp = 0, yearTemp = 0;

	msecondTemp = msecond - d.msecond;

	PL_BREAK(); // TODO

	DateTime res(yearTemp, monthTemp, dayTemp, hourTemp, minuteTemp, secondTemp, msecondTemp);
	PL_ASSERTW(res.isValid(), PL_T("Internal error"));
	return res;
}

/**
	Get UTS time offset calculated in seconds
	GMT +4 will be 14400
*/
time_t pl::DateTime::getUtsOffset()
{
	time_t zero = 24*60*60L; // Jan 2, 1900
	struct tm* timeptr;
	int gmtime_hours;

	// get the local time for Jan 2, 1900 00:00 UTC
	timeptr = localtime(&zero);
	gmtime_hours = timeptr->tm_mday < 2 ? timeptr->tm_hour : timeptr->tm_hour - 24;

	return (gmtime_hours + timeptr->tm_min*60)*60;
}

time_t pl::DateTime::get() const
{
	struct tm time;
	time_t res;

	get(time);
	res = mktime(&time);
	return res + getUtsOffset();
}

void pl::DateTime::get(struct tm& time) const
{
	time.tm_year  =  year - 1900;
	time.tm_mon   = month - 1;
	time.tm_mday  = day;
	time.tm_hour  = hour;
	time.tm_min   = minute;
	time.tm_sec   = second;
	time.tm_isdst = false;
	return;
}

void pl::DateTime::setNow()
{
	time_t val;
	time(&val);
	set(val);
	return;
}

/**
	Set datetime value as time_t
*/
void pl::DateTime::set(time_t time)
{
#if defined(PL_MSC)
	struct tm timeStruct;
	gmtime_s(&timeStruct, &time);
	set(timeStruct);
#elif defined(PL_GCC)
    struct tm timeStruct = *gmtime(&time);
	set(timeStruct);
#else
#error "Undefined compiler"
#endif
	return;
}

/**
	Set datetime value as struct tm
*/
void pl::DateTime::set(const struct tm& time)
{
	year = (pl_uint16)(time.tm_year + 1900);
	month = (pl_uint8)(time.tm_mon + 1);
	day = (pl_uint8)time.tm_mday;
	hour = (pl_uint8)time.tm_hour;
	minute = (pl_uint8)time.tm_min;
	second = (pl_uint8)time.tm_sec;
	msecond = 0;
	return;
}

/**
	Format date as a string (default is DD.M.YYYY hh:mm:ss)
	D - day (1 or 2 chars)
	DD - day (always 2 chars)
	M - month (1 or 2 chars)
	MM - month (always 2 chars)
	YY - year (last 2 chars)
	YYYY - year (4 chars)
	h - hours from 0 to 23 (1 or 2 chars)
	hh - hours from 0 to 23 (always 2 chars)
	m - minutes (1 or 2 chars)
	mm - minutes (always 2 chars)
	s - seconds (1 or 2 chars)
	ss - seconds (always 2 chars)
	dd - milliseconds (up to 4 chars)
	\x - single x char
*/
void pl::DateTime::format(pl_char* dest, const pl_char* templateText /*= NULL*/) const
{
	pl::String64 res;

	*dest = 0;
	if (templateText == NULL)
	{
		templateText = PL_T("DD.MM.YYYY hh:mm:ss");
	}

	while (*templateText != PL_T('\0'))
	{
		switch (*templateText)
		{
			case PL_T('D'):
				if (templateText[1] == PL_T('D'))
				{
					templateText++;
					res << _format(day, PL_T('0'), 2);
				}
				else
				{
					res << day;
				}
				break;
			case PL_T('M'):
				if (templateText[1] == PL_T('M'))
				{
					templateText++;
					res << _format(month, PL_T('0'), 2);
				}
				else
				{
					res << month;
				}
				break;
			case PL_T('Y'):
				if (pl_strncmp(templateText, PL_T("YYYY"), 4) == 0)
				{
					templateText += 3;
					res << _format(year, PL_T(' '), 4);
				}
				else
				{
					if (templateText[1] == PL_T('Y'))
					{
						templateText++;
					}
					res << _format(year % 100, PL_T('0'), 2);
				}
				break;
			case PL_T('h'):
				if (templateText[1] == PL_T('h'))
				{
					templateText++;
					res << _format(hour, PL_T('0'), 2);
				}
				else
				{
					res << hour;
				}
				break;
			case PL_T('m'):
				if (templateText[1] == PL_T('m'))
				{
					templateText++;
					res << _format(minute, PL_T('0'), 2);
				}
				else
				{
					res << minute;
				}
				break;
			case PL_T('s'):
				if (templateText[1] == PL_T('s'))
				{
					templateText++;
					res << _format(second, PL_T('0'), 2);
				}
				else
				{
					res << second;
				}
				break;
			case PL_T('d'):
				res << msecond;
				break;
			case PL_T('\\'):
				if (templateText[1] != PL_T('\0'))
				{
					res << templateText[1];
					templateText++;
				}
				break;
			default:
				// Put character 'as is'
				res << *templateText;
		}
		templateText++;
	}
	pl_strcpy(dest, res.get());
	return;
}

/**
	Construct date from string
	format is:
	D or DD - day (1 or 2 chars)
	M or MM - month (1 or 2 chars)
	YY or YYYY - year (last 2 chars)
	h or hh - hours from 0 to 23 (1 or 2 chars)
	m or mm - minutes (1 or 2 chars)
	s or ss - seconds (1 or 2 chars)
	dd - milliseconds (up to 4 chars)
	\x - single x char
	- : / . - dividers between digits
	T - time divider (see http://ru.wikipedia.org/wiki/ISO_8601 for details)
	U - use UTC correction
	| - optional part.

	Default ISO 8601 compatible formats are:
	"YYYYMMDD"
	"YYYY-MM"
	"YYY-MM-DD"
	"hhmm"
	"hh:mm"
	"hhmmss"
	"hh:mm:ss"
	"Uhhmm"
	"Uhh:mm"
	"hhmmssUhh"
	"hh:mm:ssUhh"
	"hhmmssUhhmm"
	"hh:mm:ssUhh:mm"
	"YYYYMMDDThhmmss|Uhh" (acceps both YYYYMMDDThhmmss and YYYYMMDDThhmmssUhh")
	"YYYY-MM-DDThh:mm:ss"
	"YYYYMMDDThhmmssUhhmm"
	"YYYY-MM-DDThh:mm:ssUhh::mm"
	"YYYY-MM-DDThh:mm:ss.dd" (by default)
*/
bool pl::DateTime::set(const pl_char* src, const pl_char** end /*= NULL*/, const pl_char* format /*= NULL*/)
{
	const pl_char* endTempValue;

	if (src == NULL)
	{
		PL_ASSERTW(FALSE, PL_T("Wrong arguments"));
		return false;
	}
	if (format == NULL)
	{
		format = PL_T("YYYY-MM-DDThh:mm:ss.dd");
	}
	if (end == NULL)
	{
		end = &endTempValue;
	}

	pl_uint16 value;
	DateTime dtDateTemp(0, 0, 0, 0, 0, 0);
	DateTime dtUtsDelta(0, 0, 0, 0, 0, 0);
	int utsSign = 0;
	const pl_char* lastGoodPos = NULL;

	while (*format != PL_T('\0'))
	{
		if (format[0] == format[1])
		{
			// Ignore differences between "H" and "HH"
			format++;
		}
		switch (*format)
		{
			case PL_T('|'):
				lastGoodPos = src;
				break;
			case PL_T('Y'):
				if (utsSign == 0 && _getNextValue(value, &src, 4))
				{
					dtDateTemp.year = value;
				}
				else goto cleanup;
				break;
			case PL_T('M'):
				if (utsSign == 0 && _getNextValue(value, &src, 2))
				{
					dtDateTemp.month = (pl_uint8)value;
				}
				else goto cleanup;
				break;
			case PL_T('D'):
				if (utsSign == 0 && _getNextValue(value, &src, 2))
				{
					dtDateTemp.day = (pl_uint8)value;
				}
				else goto cleanup;
				break;
			case PL_T('h'):
				if (_getNextValue(value, &src, 2))
				{
					if (utsSign == 0)
					{
						dtDateTemp.hour = (pl_uint8)value;
					}
					else
					{
						dtUtsDelta.hour = (pl_uint8)value;
					}
				}
				else goto cleanup;
				break;
			case PL_T('m'):
				if (_getNextValue(value, &src, 2))
				{
					if (utsSign == 0)
					{
						dtDateTemp.minute = (pl_uint8)value;
					}
					else
					{
						dtUtsDelta.minute = (pl_uint8)value;
					}
				}
				else goto cleanup;
				break;
			case PL_T('s'):
				if (utsSign == 0 && _getNextValue(value, &src, 2))
				{
					dtDateTemp.second = (pl_uint8)value;
				}
				else goto cleanup;
				break;
			case PL_T('d'):
				if (utsSign == 0 && _getNextValue(value, &src, 2))
				{
					dtDateTemp.msecond = value;
				}
				else goto cleanup;
				break;
			case PL_T('T'):
				if (utsSign == 0 && *src == PL_T('T'))
				{
					src++;
				}
				else goto cleanup;
				break;
			case PL_T('U'):
				if (utsSign != 0) goto cleanup;
				if (*src == PL_T('+'))
				{
					utsSign = 1;
				}
				else if (*src == PL_T('-'))
				{
					utsSign = -1;
				}
				else goto cleanup;
				src++;
				break;
			case PL_T('-'):
			case PL_T(':'):
			case PL_T('/'):
			case PL_T('.'):
				if (*src != *format) goto cleanup;
				src++;
				break;
			case PL_T('\\'):
				if (++format == 0) goto cleanup;
				if (*src != *format) goto cleanup;
				src++;
				break;
			default:
				PL_ASSERTW(FALSE, PL_T("Bad format"));
				goto cleanup;
		}
		format++;
	}

	// Succesfully translated
	*end = src;
	goto finalize;

cleanup:
	// An error occured, cleanup and exit
	if (lastGoodPos == NULL)
	{
		// Can't translate name
		*end = src;
		return false;
	}

	// Ignore option part, succesfully translated
	*end = lastGoodPos;

finalize:
	switch (utsSign)
	{
		case 1:
			*this = dtDateTemp + dtUtsDelta;
			break;
		case -1:
			*this = dtDateTemp - dtUtsDelta;
			break;
		default:
			PL_ASSERTE(dtUtsDelta == 0, PL_T("Internal error"));
			*this = dtDateTemp;
	}
	return true;
}

//
// End of file 'pulsar_datetime.cpp'
//
